/*
 * Copyright 2007 - The JDPF Project Team (http://www.jdpf.org) 
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 * http://www.apache.org/licenses/LICENSE-2.0 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 */
package org.jdpf.core.plugins.blocks;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.List;

import org.jdpf.core.kernel.elements.AbstractTransition;
import org.jdpf.core.kernel.elements.ITransition;
import org.jdpf.core.kernel.elements.IWiring;
import org.jdpf.core.kernel.elements.InputArc;
import org.jdpf.core.kernel.elements.OutputArc;
import org.jdpf.core.kernel.elements.Token;
import org.jdpf.core.kernel.exceptions.CoreException;
import org.jdpf.core.kernel.execution.Task;
import org.jdpf.core.kernel.logging.ILogger;
import org.jdpf.core.kernel.logging.LoggerFactory;
import org.jdpf.core.kernel.plugins.BundleToolBox;
import org.jdpf.core.kernel.plugins.IConverter;
import org.jdpf.core.kernel.plugins.IMonitoredService;
import org.jdpf.core.kernel.plugins.IPluginsFrameworkConstants;
import org.jdpf.core.plugins.datatypes.AbstractDataType;
import org.jdpf.core.plugins.datatypes.OutputDataTypesListDataType;
import org.jdpf.core.plugins.datatypes.Series;
import org.jdpf.core.plugins.exceptions.PluginException;
import org.jdpf.core.plugins.exceptions.PluginExceptionsEnum;
import org.jdpf.core.plugins.parameters.ManifestParameterValidator;
import org.jdpf.core.plugins.parameters.Parameters;
import org.jdpf.core.plugins.parameters.ParametersTypesEnum;



/**
 * The AbstractTransitionPlugin in the base implemenation of a plugin that can
 * be used in a net as a transition.
 * <p>
 * This class provides all methods to handle the wiring procedure to a net and
 * to be executed as a transition.<br>
 * The AbstratTransitionPlugin class doesn't provide any implementation of the
 * <code>fire()</code> methods required by the ITransition interface: each
 * concrete plugin should extends this class and must have its own
 * implementation of the <code>fire()</code> method. When a transition is
 * executed, its <code>fire()</code> method is called so all the plugin
 * specific code to be executed should be included in this method.
 * </p>
 * 
 * @author <a href="mailto:farina.bruno@gmail.com">Bruno Farina</a>
 * 
 */
public abstract class AbstractTransitionPlugin implements ITransition,
Runnable, IMonitoredService, ILogger {
	
	protected static final ILogger logger = LoggerFactory.getDefault().getLogger(); 

	protected Boolean _logging = false; 
	
	protected volatile double percentage;

	/**
	 * The AbstractTransition to which the plugin delegates the transition
	 * specific behaviors.
	 */
	private AbstractTransition abstractTransition;
	private String pluginId;
	private String category;
	private String serviceId;
	private List<String> outputdt;
	private IConverter converter;
	private List<List<String>> inputdtlist;
	private List<Token> tokensList;
	private Token retToken;
	private Task task;
	private volatile Thread t;
	protected volatile boolean start;

	public AbstractTransitionPlugin(AbstractTransition transition) {
		this.abstractTransition = transition;
	}

	// TODO abstract
	public boolean getLogging() {
		return _logging;
	}

	public void setLogging(String logging) {
		this._logging = Boolean.parseBoolean(logging);
	}
	
	private void validateInputToken(List inList) throws PluginException {
		for(Object obj: inList) {
			Dictionary dictionary = BundleToolBox.getDefault().getHeaders(this.pluginId);
			
			Enumeration enume = dictionary.keys();

			// Loop to get al headers which start with "Input-"
			while (enume.hasMoreElements()) {
				String key = (String) enume.nextElement();
				if (key.equals("Input-DataType")) {
					String paramManifestValue = (String) dictionary.get("Input-DataType");
					if(paramManifestValue.equals("any") || obj.getClass().getName().equals(paramManifestValue)) continue;	
					else
						throw new PluginException(PluginExceptionsEnum.InputTypeNotRecognized, this.getPluginId(), "Data type '" + obj.getClass().getName() + "' not recognized");
				}
			}
		}
	}
	
	public Token doProcess(List<Token> tokenList) throws PluginException {
		// Validation input at runtime
		for(Token token: tokenList) {
			List<AbstractDataType> inList = token.getValueList();
			if(inList != null) {
				validateInputToken(inList);
			}
		}
		Token toReturn = new Token();
		// Actual call to the algorithm
		try {
			percentage = 0.0;
			toReturn = process(tokenList);
		} catch(Exception e) {
    		this.error(this, this.getPluginId(), this.getId(), "Plugin process exception: " + e.getMessage());
    		throw new PluginException(PluginExceptionsEnum.ExecutionError, this.getPluginId(), 
    				"Plugin process exception: " + e);			
		}
		return toReturn;
	}
	
	protected Token setOutputToken(AbstractDataType adt) throws PluginException {
    	Token token = new Token(); 
    	
    	if(adt instanceof Series && ((Series)adt).isEmpty())
    		this.warning(this, this.getPluginId(), this.getId(), 
    				"The token doesn't contain any element, the component will finish the elaboration without any data.");
    	
    	token.addValue(adt);
    	return token;		
	}
	
	public void resetProcessStatus() {
		percentage=0;
	}
	
	public double getProcessStatusDouble() {
	    return (int)percentage;
	}

	public String getProcessStatusString() {
	    return "Process percentage progress: "+percentage;
	}
	
	/**
	 * Method containing the specific logic of the plugin. This method should
	 * contain all the code you need to elaborate your data with your specific
	 * plugin.
	 * 
	 * @param list -
	 *            A list containing the tokens from all input arcs ordered as
	 *            the input arcs.
	 * @return A token containing the result of the elaboration of your block.
	 * @throws URISyntaxException 
	 */
	public abstract Token process(List<Token> list) throws PluginException, URISyntaxException;

	public String toString() {
		StringBuilder sb = new StringBuilder();
		sb.append("[Abstract Plugin - ");
//		sb.append("\nTransition Id: ");
		sb.append(abstractTransition.getId());
		sb.append(" - ");
		sb.append(abstractTransition.getName());
		sb.append(" - ");
		sb.append(abstractTransition.getClass().getName() + "]");
		return sb.toString();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.jdpf.engine.kernel.elements.ITransition#addInputArc(org.jdpf.engine.kernel.elements.InputArc)
	 */
	public IWiring addInputArc(InputArc inputArc) {
		abstractTransition.addInputArc(inputArc);
		return this;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.jdpf.engine.kernel.elements.ITransition#addOutputArc(org.jdpf.engine.kernel.elements.OutputArc)
	 */
	public IWiring addOutputArc(OutputArc outputArc) {
		abstractTransition.addOutputArc(outputArc);
		return this;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.jdpf.engine.kernel.elements.ITransition#fire()
	 */
	public void fire(Task task) throws Exception {
		this.fire(true, task);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.jdpf.engine.kernel.elements.ITransition#getInputArcs()
	 */
	public List<InputArc> getInputArcs() {
		return abstractTransition.getInputArcs();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.jdpf.engine.kernel.elements.ITransition#getOutputArcs()
	 */
	public List<OutputArc> getOutputArcs() {
		return abstractTransition.getOutputArcs();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.jdpf.engine.kernel.elements.ITransition#isEnabled()
	 */
	public boolean isEnabled() {
		return abstractTransition.isEnabled();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.jdpf.engine.kernel.elements.ITransition#removeInputArc(org.jdpf.engine.kernel.elements.InputArc)
	 */
	public IWiring removeInputArc(InputArc inputArc) {
		abstractTransition.removeInputArc(inputArc);
		return this;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.jdpf.engine.kernel.elements.ITransition#removeOutputArc(org.jdpf.engine.kernel.elements.OutputArc)
	 */
	public IWiring removeOutputArc(OutputArc outputArc) {
		abstractTransition.removeOutputArc(outputArc);
		return this;
	}

	public AbstractTransition getAbstractTransition() {
		return abstractTransition;
	}

	public String getAbstractTransitionId() {
		return this.abstractTransition.getId();
	}

	public String getAbstractTransitionName() {
		return this.abstractTransition.getName();
	}
	
	public void setPluginId(String pluginId) {
		this.pluginId = pluginId;
	}

	/**
	 * Return the Bundle id from which this Transition has been obtained.
	 * @return The Bundle id
	 */
	public String getPluginId() {
		return pluginId;
	}

	protected String getId(){
		return abstractTransition.getId();
	}

	public List<Token> getInputs() {
		return abstractTransition.getInputs();
	}

	public void setOutputs(Token token) {
		abstractTransition.setOutputs(token);
	}

	public void removeInputTokens() {
		abstractTransition.removeInputTokens();
	}

	/**
	 * This method is called when the transition has to be executed.
	 * <p>
	 * The method handles parametrization and data tokens:
	 * <ul>
	 * <li>If all input tokens are data, they are processed all together and
	 * then removed from input places. A data token is produced and set as
	 * output token for this transition.</li>
	 * <li>If all input tokens are parametrization tokens then, since their
	 * content is equal, one of them is processed, causing the parametrization
	 * of the plugin. It is then set as output token and all input tokens are
	 * finally removed.</li>
	 * </ul>
	 * </p>
	 * 
	 * @see org.jdpf.core.kernel.elements.ITransition#fire(boolean)
	 */
	public void fire(boolean withAction, Task task) throws Exception {
		this.task = task;
		List<Token> list = this.getInputs();
		String tokenType = list.get(0).getType();
		if (tokenType.equals(Parameters.class.getCanonicalName())) {
			parametrize(list.get(0));
			setOutputs(list.get(0));
			removeInputTokens();
		} else if (tokenType.equals(OutputDataTypesListDataType.class.getCanonicalName())) {
			checkDataTypesValidation(list.get(0));
			if(this.category.equals(IPluginsFrameworkConstants.SPLIT)){
				setOutputs(list.get(0));
			} else if(this.category.equals(IPluginsFrameworkConstants.JOIN)){ //TODO: This Code has never been tested cause missing Aggregator block to use
				for(int i=0; i<list.size();i++){
					setOutputs(updateDataTypeToken());
				}
			} else{
				setOutputs(updateDataTypeToken());
			}
			removeInputTokens();
		} else {
			logger.log(this, this.getPluginId(), this.getId(), "Token in process.");
			
			setOutputs(startProcess(list));
			removeInputTokens();
		}
	}

	private Token startProcess(List<Token> list) {
		tokensList = list;
		retToken = null;
		t = new Thread(this);
		t.start();
		while(start&&retToken==null);
		start=false;
		t=null;
		return retToken;
	}

	public void run() {
		Thread currentThread=Thread.currentThread();
		while(start&&t==currentThread){
			try {
				retToken = doProcess(tokensList);
			} catch (Exception e) {
				this.error(this, this.getPluginId(), this.getAbstractTransitionId(), e.getMessage());
				task.abort();
			}
			start=false;
		}
	}
	
	public void stop(){
		t=null;
		start=false;
	}
	
	private Token updateDataTypeToken() {
		Token t = new Token();
		OutputDataTypesListDataType out = new OutputDataTypesListDataType();
		out.setValue(this.getOutputDataTypes());
		List<AbstractDataType> list = new ArrayList<AbstractDataType>(5);
		list.add(out);
		t.setValueList(list);
		return t;
	}

	private void checkDataTypesValidation(Token token) throws CoreException {
		OutputDataTypesListDataType odt = (OutputDataTypesListDataType) token.getValueList().get(0);
		List<String> outputTypes = (List<String>) odt.getValue();
		IConverter conv = this.getConverter();
		List<List<String>> list = this.getInputDataTypesList();
		for (List<String> inputTypes : list) {
			conv.validate(inputTypes, outputTypes);
		}
	}

	public void setConverter(IConverter converter) {
		this.converter = converter;
	}

	private IConverter getConverter() {
		return converter;
	}

	/**
	 * Method to parametrize the plugin using a parametrization token.
	 * <h4>Note:</h4>
	 * <p>
	 * Parameters can't be primitive (int, double, float, boolean)
	 * </p>
	 * 
	 * @param token
	 *            parametrization token
	 * @throws IOException 
	 */
	public void parametrize(Token token) throws IOException {
		Parameters param = (Parameters) token.getValueList().get(0);
		
		// Initializing Logging
		Object loggingObject = (param.getParameterValue(this
				.getAbstractTransitionId(), "Logging")==null)?Boolean.TRUE.toString():param.getParameterValue(this
						.getAbstractTransitionId(), "Logging");
		try {
			Method met = this.getClass().getMethod("setLogging",
					new Class<?>[] { loggingObject.getClass() });
			met.invoke(this, new Object[] { loggingObject });
			if (getLogging()) logger.log(this, this.getPluginId(), this.getId(), "Logging enabled");
		} catch (Exception e) {
			logger.error(this, this.getPluginId(), this.getId(), "Troubles in initializing logging.");
		}
		
		if (getLogging()) logger.logMainPoint(this, this.getPluginId(), this.getId(), "Plugin parametrisation started.");

		List<String> names = param.getParameterNames(this
				.getAbstractTransitionId());
		
		ManifestParameterValidator mpv = new ManifestParameterValidator(_logging, this.getPluginId());
		String paramType = null;
		for (String paramName : names) {
			if (paramName.equals("Logging")) continue;
			try {
				paramType = mpv.getParameterTypeAsInTheManifest(paramName);
				Object value = param.getParameterValue(this
						.getAbstractTransitionId(), paramName, ParametersTypesEnum.getParameterType(paramType));
				try {
					Method met = this.getClass().getMethod("set" + paramName,
							new Class<?>[] { value.getClass() });
					met.invoke(this, new Object[] { value });
					if (getLogging()) logger.log(this, this.getPluginId(), this.getId(), "Parameter '" + paramName + "' set to value " + value);
				} catch (InvocationTargetException ite) {
					logger.error(this, this.getPluginId(), this.getId(), "Troubles with reflection. " + ite.getTargetException());
				} catch (NoSuchMethodException ite) {
					logger.error(this, this.getPluginId(), this.getId(), "Method for setting parameter '" + paramName + "' with type '" + paramType + "' not found!");
				} catch (Exception e) {
					e.printStackTrace();
					logger.error(this, this.getPluginId(), this.getId(), "Parameter '" +  paramName + "' not recognized and ignored.");
				}
			
			} catch (CoreException e) {
				logger.error(this, this.getPluginId(), this.getId(), e.getMessage());
			}
		}
		if (getLogging()) logger.logMainPoint(this, this.getPluginId(), this.getId(), "Plugin parametrisation completed.");
	}

	public void setCategory(String category) {
		this.category = category;
	}

	public String getCategory() {
		return category;
	}

	public void setInputDataTypesList(List<List<String>> datatypesList) {
		this.inputdtlist = datatypesList;
	}

	public List<List<String>> getInputDataTypesList() {
		return inputdtlist;
	}

	public void setOutputDataTypes(List<String> datatypes) {
		this.outputdt = datatypes;
	}

	public List<String> getOutputDataTypes() {
		return outputdt;
	}

	/**
	 * Method to set a boolean flag to control the thread associated to this
	 * transition.
	 * 
	 * @param start -
	 *            the thread control flag
	 */
	public void setStart(boolean start) {
		this.start = start;
	}

	public void error(String text) {
		logger.error(text);
	}
	
	public void error(Object o, String text) {
		logger.error(o, text);
	}
	
	public void error(Object o, String pluginId, String text) {
		logger.error(o, pluginId, text);
	}
	
	public void error(Object o, String pluginId, String id, String text) {
		logger.error(o, pluginId, id, text);
	}
	
	public void warning(Object o, String pluginId, String id, String text) {
		logger.warning(o, pluginId, id, text);
	}
	
	public void fatal(String text) {
		logger.fatal(text);
	}
	
	public void fatal(Object o, String text) {
		logger.fatal(o, text);
	}

	public void fatal(Object o, String pluginId, String id, String text) {
		logger.fatal(o, pluginId, id, text);
	}
	
	public void log(String text) {
		if(_logging) logger.log(text);
	}

	public void log(Object o, String text) {
		if(_logging) logger.log(o, text);
	}

	public void log(Object o, String pluginId, String text) {
		if(_logging) logger.log(o, pluginId, text);
	}
	
	public void log(Object o, String pluginId, String id, String text) {
		if(_logging) logger.log(o, pluginId, id, text);
	}
	
	public void log(Object o) {
		if(_logging) logger.log(o);
	}
	
	public void logMainPoint(Object o, String pluginId, String text) {
		if(_logging) logger.logMainPoint(o, pluginId, text);
	}
	
	public void logMainPoint(Object o, String pluginId, String id, String text) {
		if(_logging) logger.logMainPoint(o,pluginId, id, text);
	}

	public String getServiceId() {
		return serviceId;
	}

	public void setServiceId(String serviceId) {
		this.serviceId = serviceId;
	}
}
